#region Using Statements
using System;
using System.IO;
using System.Collections.Generic;
using Microsoft.Xna.Framework;  
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace TileEngineComponent
{
    /// <summary>
    /// This is a game component that implements IUpdateable.
    /// </summary>
    public class TileEngine : Microsoft.Xna.Framework.DrawableGameComponent
    {
        // The TileSet class holds information on an individual tileset that has
        // been loaded for this instance of the Tile Engine.
        private class TileSet
        {
            public Texture2D t2dTexture;
            public string sFileName;
            public int iVerticalSpacing;
            public int iHorizontalSpacing;
            public int iTileWidth;
            public int iTileHeight;
            public int iTilesPerRow;
        }
        // The TileAnimation class holds all of the information necessary to
        // define a simple tile-based animation.  A tile-based animation is a
        // sequence of consecutive tiles that are shown at the designated
        // framerate to product an animation.
        private class TileAnimation
        {
            public int iStartFrame, iFrameCount, iCurrentFrame, iFrameRate;
        }
        // Local Instance Variables
        // the t2dTileSets list holds our tilesets.  Each tileset must be the
        // same size, and all tiles are also the same size.
        private List<TileSet> m_TileSets = new List<TileSet>();
        // The taAnimations list holds all animated sequences.  When tiles are
        // output, they are compared with the animated tile sequences and 
        // animated appropriately if necessary.
        private List<TileAnimation> m_taAnimations = new List<TileAnimation>();
        private int m_iTileWidth, m_iTileHeight;                  // Width and Height of a single tile
        private int m_iTileSetCount;                              // Number of Active Tilesets
        private int m_iMapWidth, m_iMapHeight;                    // Map Width and Height
        private int[,] m_iMap, m_iMapTrans, m_iMapObj, m_iMapData;// The currently active game map
        private int m_iMapX, m_iMapY, m_iMapSubX, m_iMapSubY;     // Current Map Location
        private int m_iScreenX, m_iScreenY;                       // Where we are drawing on the screen
        private int m_ScreenPxlWidth, m_ScreenPxlHeight;          // How many screen pixels are we allowed to draw
                                                                  // our map in.  
        private int m_iScreenWidth, m_iScreenHeight;              // How may tiles wide and high are we drawing
        private bool m_bDrawBase = true;                          // Will we draw the Base layer?
        private bool m_bDrawTrans = true;                         // Will we draw the Trans layer?
        private bool m_bDrawObj = true;                           // Will we draw the Obj layer?
        private bool m_bDrawFirstRowTiles = true;                 // Draw Top Row Tiles on Trans/Obj layers?
        private SpriteBatch m_spriteBatch;                        // Sprite Batch object for drawing
        private int m_iTileToDraw;                                // Used in Drawing loop to hold current tile
        private int m_iXLoc, m_iYLoc;                             // Used in Drawing loop to track tile locations
        private ContentManager m_content;                         // Content Manager Instance
        private float m_ScaleTileHeight, m_ScaleTileWidth;        // The scaled height\width of the tiles when they're
                                                                  // displayed.


        public Boolean WrapAround = false;  // Should the map wrap around?


        public int Left
        {
            // The Left property determines the left pixel position of the
            // tile engine drawing area on the display.
            get { return this.m_iScreenX; }
            set { this.m_iScreenX = value; }
        }
        public int Top
        {
            // The Top property determines the top pixel position of the
            // tile engine drawing area on the display.
            get { return this.m_iScreenY; }
            set { this.m_iScreenY = value; }
        }
        public int Width
        {
            // The Width property determines how many tiles wide will be
            // drawn by the tile engine.  Note that this property is in TILES
            // and not in PIXELS
            get { return this.m_iScreenWidth; }
            set { 
                    this.m_iScreenWidth = value;
                    this.SetMapPixels(m_ScreenPxlWidth, m_ScreenPxlHeight);
                }
        }
        public int Height
        {
            // the Height property determines how many tiles high will be
            // drawn by the tile engine.  Note that this property is in TILES
            // and not in PIXELS
            get { return this.m_iScreenHeight; }
            set { 
                    this.m_iScreenHeight = value;
                    this.SetMapPixels(m_ScreenPxlWidth, m_ScreenPxlHeight);
                }
        }
        public int TileWidth
        {
            // Determines the width of an individual tile in pixels.
            get { return this.m_iTileWidth; }
            set { this.m_iTileWidth = value; }
        }
        public int TileHeight
        {
            // Determines the height of an individual tile in pixels.
            get { return this.m_iTileHeight; }
            set { this.m_iTileHeight = value; }
        }
        public int MapX
        {
            // Determines the X map coordinate.  X=0 is the left-most tile on
            // the map.  The X coordinate represents the X value of the left-most
            // displayed map tile.
            get { return this.m_iMapX; }
            set { this.m_iMapX = value; }
        }
        public int MapY
        {
            // Determines the Y map coordinate.  Y=0 is the top-most tile on
            // the map.  The Y coordinate represents the Y value of the left-most
            // displayed map tile.
            get { return this.m_iMapY; }
            set { this.m_iMapY = value; }
        }
        public int MapWidth
        {
            // The MapWidth property is read-only since it is determined
            // by the map that is loaded.
            get { return this.m_iMapWidth; }
        }
        public int MapHeight
        {
            // The MapHeight property is read-only since it is determined
            // by the map that is loaded.
            get { return this.m_iMapHeight; }
        }
        public bool BaseVisible
        {
            get { return this.m_bDrawBase; }
            set { this.m_bDrawBase = value; }
        }
        public bool TransVisible
        {
            get { return this.m_bDrawTrans; }
            set { this.m_bDrawTrans = value; }
        }
        public bool ObjVisible
        {
            get { return this.m_bDrawObj; }
            set { this.m_bDrawObj = value; }
        }
        public bool DrawFirstRowTilesOnTransLayers
        {
            get { return this.m_bDrawFirstRowTiles; }
            set { this.m_bDrawFirstRowTiles = value; }
        }


        public TileEngine(Game game)
            : base(game)
        {
            // TODO: Construct any child components here
        }


        public void SetMapPixels(int Width, int Height)
        {
            // The ScreenWidth component determines how much 
            // screen real estate the engine gets to display
            // stuff on.  If the engine would not be able to fit
            // its required Width or Height of tiles in this space, it will
            // scale the tiles.
            m_ScreenPxlWidth = Width;
            m_ScreenPxlHeight = Height;
            int HeightPixels = this.m_iScreenHeight * this.m_iTileHeight;
            float TileScalingFactorH = (float)Height / (float)HeightPixels;
            int WidthPixels = this.m_iScreenWidth * this.m_iTileWidth;
            float TileScalingFactorW = (float)Width / (float)WidthPixels;
            m_ScaleTileHeight = TileScalingFactorH;
            m_ScaleTileWidth = TileScalingFactorW;

        }

        /// <summary>
        /// Allows the game component to perform any initialization it needs to before starting
        /// to run.  This is where it can query for any required services and load content.
        /// </summary>
        public override void Initialize()
        {
            // Start off disabled/non-visible since we need to load tilesets and
            // maps before activating.
            this.Enabled = false;
            this.Visible = false;
            // Set the tileset count to 0
            m_iTileSetCount = 0;
            m_content = new ContentManager(Game.Services);
            base.Initialize();
        }


        /// <summary>
        /// Allows the game component to update itself.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            UpdateAnimationFrames();
            base.Update(gameTime);
        }
        protected override void LoadGraphicsContent(bool loadAllContent)
        {
            if (loadAllContent)
            {
                m_spriteBatch = new SpriteBatch(GraphicsDevice);
            }
        }
        public void LoadMap(string sFileName)
        {
            // Load a map from a text file.  This is the format used in the
            // old Tile Engine Tutorials.
            StreamReader srReader;
            try
            {
                srReader = File.OpenText(sFileName);
                int iReadMapVersion = Convert.ToInt32(srReader.ReadLine());
                m_iMapHeight = Convert.ToInt32(srReader.ReadLine());
                m_iMapWidth = Convert.ToInt32(srReader.ReadLine());
                m_iMap = new int[m_iMapWidth, m_iMapHeight];
                m_iMapTrans = new int[m_iMapWidth, m_iMapHeight];
                m_iMapObj = new int[m_iMapWidth, m_iMapHeight];
                m_iMapData = new int[m_iMapWidth, m_iMapHeight];
                for (int y = 0; y < m_iMapHeight; y++)
                {
                    for (int x = 0; x < m_iMapWidth; x++)
                    {
                        m_iMap[x, y] = Convert.ToInt32(srReader.ReadLine());
                        m_iMapTrans[x, y] = Convert.ToInt32(srReader.ReadLine());
                        m_iMapObj[x, y] = Convert.ToInt32(srReader.ReadLine());
                        m_iMapData[x, y] = Convert.ToInt32(srReader.ReadLine());
                    }
                }
                srReader.Close();
            }
            catch
            {
                throw;
            }
        }
        public void AddTileset(string sFileName, int iTileWidth, int iTileHeight, int iHorizontalSpacing, int iVerticalSpacing)
        {
            // Load a new tileset from a file and add it to the tileset list.
            // Each tileset *must* be the same size, and all tiles must be the
            // same size.  
            TileSet NewTileSet = new TileSet();
            try
            {
                NewTileSet.t2dTexture = m_content.Load<Texture2D>(sFileName);
                NewTileSet.sFileName = sFileName;
                NewTileSet.iTileWidth = iTileWidth;
                NewTileSet.iTileHeight = iTileHeight;
                NewTileSet.iHorizontalSpacing = iHorizontalSpacing;
                NewTileSet.iVerticalSpacing = iVerticalSpacing;
                NewTileSet.iTilesPerRow = NewTileSet.t2dTexture.Width /
                                         (NewTileSet.iTileWidth + NewTileSet.iHorizontalSpacing);
                m_TileSets.Add(NewTileSet);
                m_iTileSetCount++;
            }
            catch
            {
                throw;
            }
        }
        public void SetMapLocation(int x, int y, int subx, int suby)
        {
            // Sets the current map location, providing the X,Y coordintate
            // as well as the Sub-Tile X and Y positions.
            m_iMapX = x;
            m_iMapY = y;
            m_iMapSubX = subx;
            m_iMapSubY = suby;
        }
        public void ScrollByPixels(int x, int y)
        {
            Boolean MoveX = true;
            Boolean MoveY = true;
            // Move the map by X pixels horizontally and Y pixels vertically.
            
            // Check if we need\can move in the direction that we're being asked to move.
            if (!WrapAround)
            {
                if ((m_iMapX <= 0) && (x <= 0))
                {
                   // m_iMapX = 0;
                    MoveX = false;
                }
                if ((m_iMapX >= m_iMapWidth - 1) && (x > 0))
                {
                   // m_iMapX = m_iMapWidth;
                    MoveX = false;
                }
                if ((m_iMapY <= 0) && (y <= 0))
                {
                    //m_iMapY = 0;
                    MoveY = false;
                }
                if ((m_iMapY >= m_iMapHeight - 1) && (y > 0))
                {
                    //m_iMapY = m_iMapHeight;
                    MoveY = false;
                }
            }
            // Now move the individual parts, accounting for moving off a tile
            // and onto another, as well as hitting screen boundaries.
            if (MoveY)
            {
                m_iMapSubY += y;
                while (m_iMapSubY >= m_iTileHeight)
                {
                    m_iMapSubY -= m_iTileHeight;
                    m_iMapY++;
                }
                while (m_iMapSubY < 0)
                {
                    m_iMapSubY += m_iTileHeight;
                    m_iMapY--;
                }
                if (m_iMapY >= m_iMapHeight)
                {
                    m_iMapY -= m_iMapHeight;
                }
                if (m_iMapY < 0)
                {
                    m_iMapY += m_iMapHeight;
                }

            }
            if (MoveX)
            {
                m_iMapSubX += x;

                while (m_iMapSubX >= m_iTileWidth)
                {
                    m_iMapSubX -= m_iTileWidth;
                    m_iMapX++;
                }

                while (m_iMapSubX < 0)
                {
                    m_iMapSubX += m_iTileWidth;
                    m_iMapX--;
                }

                if (m_iMapX >= m_iMapWidth)
                {
                    m_iMapX -= m_iMapWidth;
                }
                if (m_iMapX < 0)
                {
                    m_iMapX += m_iMapWidth;
                }
            }
        }
        public void EditMap(int x, int y, int iBase, int iTrans, int iObj, int iData)
        {
            m_iMap[x, y] = iBase;
            m_iMapTrans[x, y] = iTrans;
            m_iMapObj[x, y] = iObj;
            m_iMapData[x, y] = iData;
        }
        public int GetMapBase(int x, int y)
        {
            return m_iMap[y, x];
        }
        public int GetMapTrans(int x, int y)
        {
            return m_iMapTrans[y, x];
        }
        public int GetMapObj(int x, int y)
        {
            return m_iMapObj[y, x];
        }
        public int GetMapData(int x, int y)
        {
            return m_iMapData[y, x];
        }
        public void AddTileAnimation(int iStartFrame, int iFrameCount, int iFrameRate)
        {
            // Define a new tileset animation.  Tiles in an animation must
            // be consecutive and must all reside on the same tileset.
            TileAnimation thisAnimation = new TileAnimation();
            thisAnimation.iStartFrame = iStartFrame;
            thisAnimation.iFrameCount = iFrameCount;
            thisAnimation.iFrameRate = iFrameRate;
            thisAnimation.iCurrentFrame = 0;
            m_taAnimations.Add(thisAnimation);
        }
        private bool IsAnimatedTile(int iTile)
        {
            foreach (TileAnimation thisAnimation in m_taAnimations)
            {
                if (thisAnimation.iStartFrame == iTile)
                {
                    return true;
                }
            }
            return false;
        }
        private int GetAnimatedFrame(int iTile)
        {
            foreach (TileAnimation thisAnimation in m_taAnimations)
            {
                if (thisAnimation.iStartFrame == iTile)
                {
                    return thisAnimation.iStartFrame + (thisAnimation.iCurrentFrame / thisAnimation.iFrameRate);
                }
            }
            return 0;
        }
        private void UpdateAnimationFrames()
        {
            for (int x = 0; x < m_taAnimations.Count; x++)
            {
                m_taAnimations[x].iCurrentFrame++;
                if (m_taAnimations[x].iCurrentFrame > ((m_taAnimations[x].iFrameCount * m_taAnimations[x].iFrameRate)
                                                  + (m_taAnimations[x].iFrameRate - 1)))
                {
                    m_taAnimations[x].iCurrentFrame = 0;
                }
            }
        }
        private Rectangle GetTileRectangle(int iTileNumber)
        {
            // Returns a rectangle representing the location on the tileset that
            // contains the requested tile.
            // The 10000's digit determines the tileset number.  The remainder 
            // determines the tile within the set, so seperate out the remainder 
            // to determine where to pull the rectangle from.
            int iTile = iTileNumber % 10000;
            int iTileSet = iTileNumber / 10000;
            // Return a rectangle representing a location of a tile on the tileset
            return new Rectangle(
              (iTile % m_TileSets[iTileSet].iTilesPerRow) * m_TileSets[iTileSet].iTileWidth,
              (iTile / m_TileSets[iTileSet].iTilesPerRow) * m_TileSets[iTileSet].iTileHeight,
              m_TileSets[iTileSet].iTileWidth,
              m_TileSets[iTileSet].iTileHeight
            );
        }
      
        public override void Draw(GameTime gameTime)
        {
            m_spriteBatch.Begin(SpriteBlendMode.None);
            // Draw the base layer of the map
            if (m_bDrawBase)
            {
                for (int x = 0; x < m_iScreenWidth; x++)

                {
                    for (int y = 0; y < m_iScreenHeight; y++)

                    {
                        m_iXLoc = x + m_iMapX;
                        m_iYLoc = y + m_iMapY;
                        // Account for map wrap-arounds
                        if (m_iXLoc >= m_iMapHeight)
                        {
                            m_iXLoc -= m_iMapWidth;
                        }
                        if (m_iXLoc < 0)
                        {
                            m_iXLoc += m_iMapWidth;
                        }
                        if (m_iYLoc >= m_iMapHeight)
                        {
                            m_iYLoc -= m_iMapHeight;
                        }
                        if (m_iYLoc < 0)
                        {
                            m_iYLoc += m_iMapHeight;
                        }
                        m_iTileToDraw = m_iMap[m_iXLoc, m_iYLoc];
                        if (IsAnimatedTile(m_iTileToDraw))
                        {
                            m_iTileToDraw = GetAnimatedFrame(m_iTileToDraw);
                        }
                        if (m_iTileToDraw >= 0)
                        // Only draw tiles > 0 since TileStudio uses -1
                        // to indicate an empty tile.
                        {
                            // Draw the tile.  We divide the tile number by 10000 to
                            // determine what tileset the tile is on (0-9999=tileset 0,
                            // 10000-19999=Tileset 2, etc)
                            
                            /*                            m_spriteBatch.Draw(m_TileSets[(m_iTileToDraw / 10000)].t2dTexture,
                                             new Rectangle(((x * m_iTileWidth) + m_iScreenX) - m_iMapSubX,
                                             ((y * m_iTileHeight) + m_iScreenY) - m_iMapSubY,
                                             m_TileSets[(m_iTileToDraw / 10000)].iTileWidth,
                                             m_TileSets[(m_iTileToDraw / 10000)].iTileHeight),
                                             GetTileRectangle(m_iTileToDraw),
                                             Color.White); */
                            // texture, destination (also does scaling), source, color.
                            float X = (((x * m_iTileWidth) + m_iScreenX) - m_iMapSubX) * m_ScaleTileWidth;
                            float Y = (((y * m_iTileHeight) + m_iScreenY) - m_iMapSubY) * m_ScaleTileHeight;
                            float X_size = (m_TileSets[(m_iTileToDraw / 10000)].iTileWidth) * m_ScaleTileWidth;
                            float Y_size = (m_TileSets[(m_iTileToDraw / 10000)].iTileHeight) * m_ScaleTileHeight;

                            m_spriteBatch.Draw(m_TileSets[(m_iTileToDraw / 10000)].t2dTexture,
                                             new Rectangle(System.Convert.ToInt32(X),
                                             System.Convert.ToInt32(Y),
                                             System.Convert.ToInt32(X_size),
                                             System.Convert.ToInt32(Y_size)),
                                             GetTileRectangle(m_iTileToDraw),
                                             Color.White);
                        }
                    }
                }
            }
            m_spriteBatch.End();
            m_spriteBatch.Begin(SpriteBlendMode.AlphaBlend);
            // Draw transitions and Objects layers of the map
            for (int x = 0; x < m_iScreenWidth; x++)
            {
                for (int y = 0; y < m_iScreenHeight; y++)                
                {
                    // texture, destination (also does scaling), source, color.
                    float X = (((x * m_iTileWidth) + m_iScreenX) - m_iMapSubX) * m_ScaleTileWidth;
                    float Y = (((y * m_iTileHeight) + m_iScreenY) - m_iMapSubY) * m_ScaleTileHeight;
                    float X_size = (m_TileSets[(m_iTileToDraw / 10000)].iTileWidth) * m_ScaleTileWidth;
                    float Y_size = (m_TileSets[(m_iTileToDraw / 10000)].iTileHeight) * m_ScaleTileHeight;
                    Rectangle recDest = new Rectangle(System.Convert.ToInt32(X),
                                             System.Convert.ToInt32(Y),
                                             System.Convert.ToInt32(X_size),
                                             System.Convert.ToInt32(Y_size));
                    m_iXLoc = x + m_iMapX;
                    m_iYLoc = y + m_iMapY;
                    if (m_iXLoc >= m_iMapHeight)
                    {
                        m_iXLoc -= m_iMapWidth;
                    }
                    if (m_iXLoc < 0)
                    {
                        m_iXLoc += m_iMapWidth;
                    }
                    if (m_iYLoc >= m_iMapHeight)
                    {
                        m_iYLoc -= m_iMapHeight;
                    }
                    if (m_iYLoc < 0)
                    {
                        m_iYLoc += m_iMapHeight;
                    }
                    if (m_bDrawTrans)
                    {
                        m_iTileToDraw = m_iMapTrans[m_iXLoc, m_iYLoc];
                        if (IsAnimatedTile(m_iTileToDraw))
                        {
                            m_iTileToDraw = GetAnimatedFrame(m_iTileToDraw);
                        }
                        if (m_iTileToDraw >= 0 && ((m_iTileToDraw >
                            m_TileSets[m_iTileToDraw / 10000].iTilesPerRow) || m_bDrawFirstRowTiles))
                        {
                            m_spriteBatch.Draw(m_TileSets[m_iTileToDraw / 10000].t2dTexture,
                                               recDest, GetTileRectangle(m_iTileToDraw),
                                               Color.White);
                        }
                    }
                    if (m_bDrawObj)
                    {
                        m_iTileToDraw = m_iMapObj[m_iXLoc, m_iYLoc];
                        if (IsAnimatedTile(m_iTileToDraw))
                        {
                            m_iTileToDraw = GetAnimatedFrame(m_iTileToDraw);
                        }
                        if (m_iTileToDraw >= 0 && ((m_iTileToDraw >
                            m_TileSets[m_iTileToDraw / 1000].iTilesPerRow) || m_bDrawFirstRowTiles))
                        {
                            m_spriteBatch.Draw(m_TileSets[m_iTileToDraw / 10000].t2dTexture,
                                recDest, GetTileRectangle(m_iTileToDraw),
                                Color.White);
                        }
                    }
                }
            }
            m_spriteBatch.End();
            base.Draw(gameTime);
        }
    }
}


